<template>
  <div class="w-menu-wrap" ref="wrapperRef">
    <slot name="activator" :on="eventHandlers"></slot>
    <transition :name="transitionName">
      <div class="w-menu" v-if="custom" ref="menuRef" v-show="showMenu" :class="classes" :style="styles">
        <slot></slot>
      </div>
      <w-card class="w-menu" v-else ref="menuRef" v-show="showMenu" :tile="tile" :title-class="titleClass"
        :content-class="contentClass" :shadow="shadow" :no-border="noBorder" :class="classes" :style="styles"><template
          v-if="$slots.title" #title>
          <slot name="title"></slot>
        </template><template v-if="$slots.actions" #actions>
          <slot name="actions"></slot>
        </template>
        <slot></slot>
      </w-card>
    </transition>
    <w-overlay v-if="overlay" ref="overlayRef" :model-value="showMenu" :persistent="persistent"
      :z-index="(zIndex || 200) - 1" @update:model-value="showMenu = false"></w-overlay>
  </div>
</template>

<script>
  /**
   * Complexity of this component: Vue 2.x can only mount 1 single root element, but we don't
   * want to wrap the activator as it may break the layout.
   * Another simpler way would be to append the menu inside the activator, but some HTML tags
   * can't have children like <input>.
   * So a solution is to mount both the activator element and the menu in a wrapper then unwrap
   * and move the menu elsewhere in the DOM.
   */

  import { consoleWarn } from "../../utils/console";

  // const marginFromWindowSide = 4 // Amount of px from a window side, instead of overflowing.
  import {
    ref,
    reactive,
    computed,
    getCurrentInstance,
    onMounted,
    onBeforeUnmount,
    watch,
    toRefs
  } from "vue";
  export default {
    name: "w-menu",

    props: {
      modelValue: {}, // Show or hide.
      showOnHover: { type: Boolean },
      hideOnMenuClick: { type: Boolean },
      color: { type: String },
      bgColor: { type: String },
      shadow: { type: Boolean },
      custom: { type: Boolean }, // Include a w-card or not. It does by default.
      tile: { type: Boolean },
      round: { type: Boolean },
      noBorder: { type: Boolean },
      transition: { type: String },
      menuClass: { type: String },
      titleClass: { type: String },
      contentClass: { type: String },
      // Position.
      detachTo: {},
      fixed: { type: Boolean },
      top: { type: Boolean },
      bottom: { type: Boolean },
      left: { type: Boolean },
      right: { type: Boolean },
      alignTop: { type: Boolean },
      alignBottom: { type: Boolean },
      alignLeft: { type: Boolean },
      alignRight: { type: Boolean },
      zIndex: { type: [Number, String, Boolean] },
      minWidth: { type: [Number, String] }, // can be like: `40`, `5em`, `activator`.
      overlay: { type: Boolean },
      persistent: { type: Boolean },
      noPosition: { type: Boolean }
    },

    emits: ["input", "update:modelValue", "open", "close"],
    setup(props, ctx) {
      const instance = getCurrentInstance();
      // The menu computed top & left coordinates.
      const menuCoordinates = reactive({
        top: 0,
        left: 0
      });
      const showMenu = ref(false);
      const activatorEl = ref(null);
      const activatorWidth = ref(0);
      const menuEl = ref(null);
      const timeoutId = ref(null);
      const overlayEl = ref(null);
      // refs
      const wrapperRef = ref(null);
      const menuRef = ref(null);
      const overlayRef = ref(null);

      onMounted(() => {
        activatorEl.value = wrapperRef.value.firstElementChild;
        overlayEl.value = props.overlay ? overlayRef.value.$el : null;
        insertMenu();

        if (props.modelValue) {
          toggle({ type: "click", target: activatorEl.value });
        }
      });

      onBeforeUnmount(() => {
        removeMenu();
        // el.remove() doesn't work on IE11.
        if (props.overlay && overlayEl.value.parentNode) {
          overlayEl.value.parentNode.removeChild(overlayEl.value);
        }
        if (activatorEl.value && activatorEl.value.parentNode) {
          activatorEl.value.parentNode.removeChild(activatorEl.value);
        }
      });

      const transitionName = computed(() => {
        return props.transition || `scale-fade`;
      });

      // DOM element to attach menu to.
      const detachToTarget = computed(() => {
        let target = props.detachTo || ".w-app";
        if (target === true) target = ".w-app";
        else if (target && !["object", "string"].includes(typeof target))
          target = ".w-app";
        else if (typeof target === "object" && !target.nodeType) {
          target = ".w-app";
          consoleWarn(
            "Invalid node provided in w-menu `detach-to`. Falling back to .w-app.",
            instance.ctx
          );
        }
        if (typeof target === "string") {
          target = document.querySelector(target);
        }

        if (!target) {
          consoleWarn(
            `Unable to locate ${props.detachTo ? `target ${props.detachTo}` : ".w-app"
            }`,
            instance.ctx
          );
          target = document.querySelector(".w-app");
        }

        return target;
      });

      // DOM element that will receive the menu.
      const menuParentEl = computed(() => {
        return detachToTarget.value;
      });

      const position = computed(() => {
        return (
          (props.top && "top") ||
          (props.bottom && "bottom") ||
          (props.left && "left") ||
          (props.right && "right") ||
          "bottom"
        );
      });
      const menuMinWidth = computed(() => {
        if (props.minWidth === "activator")
          return activatorWidth.value ? `${activatorWidth.value}px` : 0;
        else
          return isNaN(props.minWidth)
            ? props.minWidth
            : props.minWidth
              ? `${props.minWidth}px`
              : 0;
      });

      const alignment = computed(() => {
        return (
          ((props.top || props.bottom) && props.alignLeft && "left") ||
          ((props.top || props.bottom) && props.alignRight && "right") ||
          ((props.left || props.right) && props.alignTop && "top") ||
          ((props.left || props.right) && props.alignBottom && "bottom") ||
          ""
        );
      });

      const classes = computed(() => {
        return {
          [props.color]: props.color,
          [`${props.bgColor}--bg`]: props.bgColor,
          [props.menuClass]: props.menuClass,
          [`w-menu--${position.value}`]: true,
          [`w-menu--align-${alignment.value}`]: alignment.value,
          "w-menu--tile": props.tile,
          "w-menu--card": !props.custom,
          "w-menu--round": props.round,
          "w-menu--shadow": props.shadow,
          "w-menu--fixed": props.fixed
        };
      });
      const styles = computed(() => {
        return {
          zIndex:
            props.zIndex ||
            props.zIndex === 0 ||
            (props.overlay && !props.zIndex && 200) ||
            null,
          top: (menuCoordinates.top && `${~~menuCoordinates.top}px`) || null,
          left: (menuCoordinates.left && `${~~menuCoordinates.left}px`) || null,
          minWidth: (props.minWidth && menuMinWidth.value) || null
        };
      });

      const eventHandlers = computed(() => {
        let handlers = {};
        if (props.showOnHover) {
          handlers = {
            focus: toggle,
            blur: toggle,
            mouseenter: toggle,
            mouseleave: toggle
          };
          if ("ontouchstart" in window) {
            handlers.click = toggle;
          }
        } else {
          handlers = { click: toggle };
        }
        return handlers;
      });

      function toggle(e) {
        let shouldShowMenu = showMenu.value;
        if ("ontouchstart" in window && props.showOnHover && e.type === "click") {
          shouldShowMenu = !shouldShowMenu;
        }
        if (e.type === "click" && !props.showOnHover) {
          shouldShowMenu = !shouldShowMenu;
        }
        if (e.type === "mouseenter" && props.showOnHover) {
          shouldShowMenu = true;
        }
        if (e.type === "mouseleave" && props.showOnHover) {
          shouldShowMenu = false;
        }
        timeoutId.value = clearTimeout(timeoutId.value);
        // Open the menu.
        if (shouldShowMenu) {
          if (props.minWidth === "activator") {
            activatorWidth.value = activatorEl.value.offsetWidth;
          }

          if (!props.noPosition) {
            computeMenuPosition(e);
          }

          // In `getCoordinates` accessing the menu computed styles takes a few ms (less than 10ms),
          // if we don't postpone the Menu apparition it will start transition from a visible menu and
          // thus will not transition.
          timeoutId.value = setTimeout(() => {
            ctx.emit("update:modelValue", (showMenu.value = true));
            ctx.emit("input", true);
            ctx.emit("open");
          }, 10);

          if (!props.persistent) {
            document.addEventListener("mousedown", onOutsideMousedown);
          }
          if (!props.noPosition) {
            window.addEventListener("resize", onResize);
          }
        }

        // Close the menu.
        else {
          ctx.emit("update:modelValue", (showMenu.value = false));
          ctx.emit("input", false);
          ctx.emit("close");
          // Remove the mousedown listener if the menu got closed without a mousedown outside of the menu.
          document.removeEventListener("mousedown", onOutsideMousedown);
          window.removeEventListener("resize", onResize);
        }
      }

      function onOutsideMousedown(e) {
        if (!menuEl.value.contains(e.target)) {
          ctx.emit("update:modelValue", (showMenu.value = false));
          ctx.emit("input", false);
          ctx.emit("close");
          document.removeEventListener("mousedown", onOutsideMousedown);
          window.removeEventListener("resize", onResize);
        }
      }

      function onResize() {
        if (props.minWidth === "activator") {
          activatorWidth.value = activatorEl.value.offsetWidth;
        }
        computeMenuPosition();
      }

      function getCoordinates(e) {
        // Get the activator coordinates relative to window.
        const { top, left, width, height } = (e
          ? e.target
          : activatorEl.value
        ).getBoundingClientRect();
        let coords = { top, left, width, height };

        // If absolute position, adjust top & left.
        if (!props.fixed) {
          const {
            top: targetTop,
            left: targetLeft
          } = menuParentEl.value.getBoundingClientRect();
          const computedStyles = window.getComputedStyle(
            menuParentEl.value,
            null
          );
          coords = {
            ...coords,
            top:
              top -
              targetTop +
              menuParentEl.value.scrollTop -
              parseInt(computedStyles.getPropertyValue("border-top-width")),
            left:
              left -
              targetLeft +
              menuParentEl.value.scrollLeft -
              parseInt(computedStyles.getPropertyValue("border-left-width"))
          };
        }

        return coords;
      }

      function computeMenuPosition(e) {
        // Get the activator coordinates.
        let { top, left, width, height } = getCoordinates(e);

        // 1. First display the menu but hide it (So we can get its dimension).
        // --------------------------------------------------
        menuEl.value.style.visibility = "hidden";
        menuEl.value.style.display = "flex";
        const computedStyles = window.getComputedStyle(menuEl.value, null);

        // 2. Position the menu top, left, right, bottom and apply chosen alignment.
        // --------------------------------------------------
        // Subtract half or full activator width or height and menu width or height according to the
        // menu alignment.
        // Note: the menu position relies on transform translate, the custom animation may override the
        // css transform property so do without it i.e. no translateX(-50%), and recalculate top & left
        // manually.
        switch (position.value) {
          case "top": {
            top -= menuEl.value.offsetHeight;
            if (props.alignRight) {
              // left: 100% of activator.
              left +=
                width -
                menuEl.value.offsetWidth +
                parseInt(computedStyles.getPropertyValue("border-right-width"));
            } else if (!props.alignLeft) {
              left += (width - menuEl.value.offsetWidth) / 2; // left: 50% of activator - half menu width.
            }
            break;
          }
          case "bottom": {
            top += height;
            if (props.alignRight) {
              // left: 100% of activator.
              left +=
                width -
                menuEl.value.offsetWidth +
                parseInt(computedStyles.getPropertyValue("border-right-width"));
            } else if (!props.alignLeft)
              left += (width - menuEl.value.offsetWidth) / 2; // left: 50% of activator - half menu width.
            break;
          }
          case "left": {
            left -= menuEl.value.offsetWidth;
            if (props.alignBottom) top += height - menuEl.value.offsetHeight;
            else if (!props.alignTop)
              top += (height - menuEl.value.offsetHeight) / 2; // top: 50% of activator - half menu height.
            break;
          }
          case "right": {
            left += width;
            if (props.alignBottom) {
              top +=
                height -
                menuEl.value.offsetHeight +
                parseInt(computedStyles.getPropertyValue("margin-top"));
            } else if (!props.alignTop) {
              top +=
                (height - menuEl.value.offsetHeight) / 2 + // top: 50% of activator - half menu height.
                parseInt(computedStyles.getPropertyValue("margin-top"));
            }
            break;
          }
        }

        // 3. Keep fully in viewport.
        // @todo: do this.
        // --------------------------------------------------
        // if (this.position === 'top' && ((top - this.menuEl.offsetHeight) < 0)) {
        //   const margin = - parseInt(computedStyles.getPropertyValue('margin-top'))
        //   top -= top - this.menuEl.offsetHeight - margin - marginFromWindowSide
        // }
        // else if (this.position === 'left' && left - this.menuEl.offsetWidth < 0) {
        //   const margin = - parseInt(computedStyles.getPropertyValue('margin-left'))
        //   left -= left - this.menuEl.offsetWidth - margin - marginFromWindowSide
        // }
        // else if (this.position === 'right' && left + width + this.menuEl.offsetWidth > window.innerWidth) {
        //   const margin = parseInt(computedStyles.getPropertyValue('margin-left'))
        //   left -= left + width + this.menuEl.offsetWidth - window.innerWidth + margin + marginFromWindowSide
        // }
        // else if (this.position === 'bottom' && top + height + this.menuEl.offsetHeight > window.innerHeight) {
        //   const margin = parseInt(computedStyles.getPropertyValue('margin-top'))
        //   top -= top + height + this.menuEl.offsetHeight - window.innerHeight + margin + marginFromWindowSide
        // }

        // 4. Hide the menu again so the transition happens correctly.
        // --------------------------------------------------
        menuEl.value.style.visibility = null;

        // The menu coordinates are also recalculated while resizing window with open menu: keep the menu visible.
        if (!showMenu.value) {
          menuEl.value.style.display = "none";
        }

        // menuCoordinates = { top, left };
        menuCoordinates.top = top;
        menuCoordinates.left = left;
      }

      function insertMenu() {
        const wrapper = wrapperRef.value;
        menuEl.value = menuRef.value.$el || menuRef.value;
        // Unwrap the activator element.
        wrapper.parentNode.insertBefore(activatorEl.value, wrapper);

        // Unwrap the overlay
        if (props.overlay) {
          wrapper.parentNode.insertBefore(overlayEl.value, wrapper);
        }

        // Move the menu elsewhere in the DOM.
        // wrapper.parentNode.insertBefore(this.menuEl, wrapper)
        detachToTarget.value.appendChild(menuEl.value);
      }

      function removeMenu() {
        // el.remove() doesn't work on IE11.
        if (menuEl.value && menuEl.value.parentNode)
          menuEl.value.parentNode.removeChild(menuEl.value);
      }

      watch(
        () => props.modelValue,
        value => {
          if (!!value !== showMenu.value) {
            toggle({ type: "click", target: activatorEl.value });
          }
        }
      );
      watch(
        () => props.detachTo,
        () => {
          removeMenu();
          insertMenu();
        }
      );
      return {
        menuCoordinates,
        showMenu,
        activatorEl,
        activatorWidth,
        menuEl,
        timeoutId,
        // refs
        wrapperRef,
        menuRef,
        overlayRef,
        // computed
        transitionName,
        detachToTarget,
        menuParentEl,
        position,
        menuMinWidth,
        alignment,
        classes,
        styles,
        eventHandlers,
        // methods
        toggle,
        onOutsideMousedown,
        onResize,
        getCoordinates,
        computeMenuPosition,
        insertMenu,
        removeMenu
      };
    }
  };
</script>